package org.pathlet.core.impl;

import static org.pathlet.core.PathletConstants.CONTAINER_SCOPE;
import static org.pathlet.core.PathletConstants.DEFAULT_SCOPES;
import static org.pathlet.core.PathletConstants.DEFAULT_FACTORY_PATH;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.pathlet.core.config.ConfigException;
import org.pathlet.core.config.ConfigInstanceEvent;
import org.pathlet.core.config.ConfigManager;
import org.pathlet.core.config.ResourceConfig;
import org.pathlet.core.config.def.AdviceConfig;
import org.pathlet.core.config.def.BeanResourceConfig;
import org.pathlet.core.*;
import org.pathlet.core.Module;
import org.pathlet.core.exception.ResourceDuplicationException;
import org.pathlet.core.exception.ResourceNotFoundException;

public class DefaultPathletContainer implements PathletContainer {

	private ConfigManager configManager;

	private ModuleManager moduleManager = null;

	private String[] scopes;

	private PathClassLoader pathClassLoader;

	//Default container PathClassLoader, this will use the current ClassLoader to load class.
	//private PathClassLoader defaultPathletClassLoader = null;

	private InstanceSpace containerSpace = null;

	private Map<Path, PathResourceFactory> factoriesMap = null;

	private Map<Path, ConfigResourceFactory> configFactoriesMap = null;

	private PathletResourceCache resourceCache = null;

	private List<ResourceInstanceEvent> instanceEvents = null;

	private List<SpaceInstanceEvent> spaceInstanceEvents = null;

	//每一个InstanceSpace实例会绑定一个对象, 为了能够快算定位到不同对象和生命周期绑定的InstanceSpace,
	//此属性设计为两级式的Map对象:
	//  第一级的key为scope name, 第一级的value是在这个scope下的对象和InstanceSpace的Map对象。
	//  第二级即第一级的value中存放的Map对象：
	//     key为绑定对象(每个namespace都会绑定到一个对象上, 如：web应用中的每个ServletRequest绑定到一个InstanceSpace实例)。
	//     value为InstanceSpace实例。
	//如：在web应用中我们会对每个ServletRequest绑定一个InstanceSpace
	private Map<String, Map<Object, InstanceSpace>> scopeSpaceMap = null;

	//Store the alias mapping. Map from the Map.key to Map.value
	private Map<Path, Path> aliases = null;

	//Store the paths from the bean configure property "initInstance"
	//The key is the scope name, value is the list of paths of the "initInstance" beans.
	private Map<String, List<Path>> initInstancePaths = null;

	public DefaultPathletContainer(ConfigManager cfgManager) {
		this(cfgManager, DEFAULT_SCOPES);
	}

	public DefaultPathletContainer(ConfigManager cfgManager, String[] scopes) {
		this.configManager = cfgManager;
		this.scopes = scopes;
		checkScopes(scopes);

		this.resourceCache = new PathletResourceCache();
		this.factoriesMap =  new LinkedHashMap<Path, PathResourceFactory>();
		this.configFactoriesMap =  new LinkedHashMap<Path, ConfigResourceFactory>();
		this.instanceEvents =  new ArrayList<ResourceInstanceEvent>();
		this.spaceInstanceEvents = new ArrayList<SpaceInstanceEvent>();
		this.pathClassLoader = new SimplePathClassLoader();

		//Set create the ModuleManager, if has the modules settings.
		if(configManager.getModules() != null && configManager.getModules().size() > 0) {
			this.moduleManager = new DefaultModuleManager(configManager.getModules(), this);
			this.pathClassLoader = moduleManager.getPathClassLoader();
			this.spaceInstanceEvents.add(moduleManager.getSpaceInstanceEvent());
		}

		//Add the default ResourceInstanceEvent objects.
		//this.instanceEvents.add(moduleManager.getResourceInstanceEvent());
		this.instanceEvents.add(new AopInstanceEvent(this.configManager));
		this.instanceEvents.add(new ConfigInstanceEvent());

		//Initialize the scopeSpaceMap subordinate Map(s): Map<Object, InstanceSpace>.
		//Each scope will create a Map<Object, InstanceSpace> and put into the scopeSpaceMap.
		this.scopeSpaceMap = new LinkedHashMap<String, Map<Object, InstanceSpace>>(scopes.length);
		for(String scope : scopes) {
			Map<Object, InstanceSpace> spaceBindingMap = new LinkedHashMap<Object, InstanceSpace>();
			this.scopeSpaceMap.put(scope, spaceBindingMap);
		}


		this.aliases = configManager.getAliases(); //The aliases should be initialized before the creators creation
		Collection<BeanResourceConfig> beanConfigs = configManager.getBeanConfigs();

		//Skip the initInstance process, because the initInstancePaths has not initialized yet.
		//Create the default and single container InstanceSpace and set the parent as the settingSpace,
		//and bind the container with containerSpace
		this.containerSpace = new DefaultInstanceSpace(this, null, CONTAINER_SCOPE, this, this.spaceInstanceEvents);
		scopeSpaceMap.get(CONTAINER_SCOPE).put(this, this.containerSpace);

		//Store the bean configure paths, which contains the property initInstance=true.
		this.initInstancePaths = new HashMap<String, List<Path>>();
		for(BeanResourceConfig beanConfig : beanConfigs) {
			if(beanConfig.isInitInstance()) {
				String scope = beanConfig.getScope();
				List<Path> paths = initInstancePaths.get(scope);
				if(paths == null) {
					paths = new ArrayList<Path>();
					initInstancePaths.put(scope, paths);
				}

				paths.add(beanConfig.getPath());
			}
		}

		//create and initialize and all ResourceFactory(s)
		initResourceFactories(beanConfigs);

		//create and initialize all BeanResource(s) in the configManager
		initBeanConfigs(beanConfigs);

		//Instance the advisor one by one from the configManager.
		Map<Path, List<AdviceConfig>> advices = configManager.getAdvices();
		if(advices != null) {
			for(Path advicePath : advices.keySet()) {
				Object advisorObject = getInstance(advicePath);
				if(advisorObject == null) {
					throw new ConfigException("Failed to load instance the to advice path='" + advicePath + "', Please check your advice configuration!");
				}
			}
		}

		//Instance the "initInstance" property beans, the getInstance(Path) method will be invoke to instance it.
		toInitInstance(this.containerSpace);

		//Start all modules, if any exists.
		if(moduleManager != null) {
			Collection<Module> modules = moduleManager.getModules();
			for(Module module : modules) {
				moduleManager.startModule(module.getId());
			}
		}

	}

	private void initResourceFactories(Collection<BeanResourceConfig> beanConfigs) {
		//Create a BeanResourceFactory for settings.
		//It is a temporary ResourceFacotry, will be remove after the settingSpace has been completely created.
		ConfigBeanResourceFactory defaultResourceFactory = new ConfigBeanResourceFactory();
		defaultResourceFactory.setContainer(this);
		defaultResourceFactory.setDefaultScope(CONTAINER_SCOPE);
		configFactoriesMap.put(DEFAULT_FACTORY_PATH, defaultResourceFactory);

		//Initialize ConfigResourceFacotory(s)
		for(BeanResourceConfig beanConfig : beanConfigs) {
			//Find the ConfigResourceFactory(s) beans and add them into factoriesMap
			if(CONTAINER_SCOPE.equals(beanConfig.getScope())) {
				PathletResource settingResource = getResource(beanConfig.getPath());
				if(settingResource != null && BeanResource.class.isAssignableFrom(settingResource.getClass())) {
					Class beanClazz = ((BeanResource)settingResource).getTargetType();

					if (ConfigResourceFactory.class.isAssignableFrom(beanClazz)) {
						ConfigResourceFactory factory = containerSpace.getInstance(beanConfig.getPath());
						factory.setContainer(this);
						System.out.println("Add the ConfigResourceFactory for path=" + beanConfig.getPath() + ", factory=" + factory.getClass().getName());
						configFactoriesMap.put(beanConfig.getPath(), factory);
					}
				}
			}
		}

		//Initialize PathResourceFacotory(s)
		for(BeanResourceConfig beanConfig : beanConfigs) {

			//Find the PathResourceFactory(s) beans and add them into factoriesMap
			if(CONTAINER_SCOPE.equals(beanConfig.getScope())) {
				PathletResource settingResource = getResource(beanConfig.getPath());
				if(settingResource != null && BeanResource.class.isAssignableFrom(settingResource.getClass())) {
					Class beanClazz = ((BeanResource)settingResource).getTargetType();

					if (PathResourceFactory.class.isAssignableFrom(beanClazz)) {
						PathResourceFactory factory = containerSpace.getInstance(beanConfig.getPath());
						factory.setContainer(this);
						System.out.println("Add the PathResourceFactory for path=" + beanConfig.getPath() + ", factory=" + factory.getClass().getName());
						factoriesMap.put(beanConfig.getPath(), factory);
					}
				}
			}
		}
	}

	// 把所有的BeanResourceConfig都初始化，逐一解析并把解析后的BeanResource放入到缓存中
	private void initBeanConfigs(Collection<BeanResourceConfig> beanConfigs) {
		for(BeanResourceConfig beanConfig : beanConfigs) {
			Path beanPath = beanConfig.getPath();
			if(resourceCache.contains(beanPath) == false) {
				getResource(beanPath); //Actually create the BeanResource instance and put it into the cache.
			}
			getResource(beanConfig.getPath());
		}
	}

	public ConfigManager getConfigManager() {
		return configManager;
	}

	public ModuleManager getModuleManager() {
		return moduleManager;
	}

	public String[] getScopes() {
		return scopes;
	}

	public PathClassLoader getPathClassLoader() {
		return pathClassLoader;
	}

	public Map<Path, Path> getAliases() {
		return aliases;
	}

	public synchronized PathletResource getResource(Path resourcePath) {
		PathletResource resource = resourceCache.get(resourcePath);
		if(resource == null) {
			resource = fetchResource(resourcePath);
			if(resource != null) {
				resourceCache.put(resourcePath, resource);
			}
			else {
				throw new ResourceNotFoundException(resourcePath);
			}
		}

		return resource;
	}

	@Override
	public Path getPathByType(String type) {
		Path path = resourceCache.getPath(type);
		if(path == null) {
			for(PathResourceFactory factory : factoriesMap.values()) {
				if(factory.isTypeMatch(type)) {
					path = factory.getPathByType(type);
					if (path != null) {
						getResource(path);
						break;
					}
				}
			}
		}

		return path;
	}

	public synchronized void addResource(PathletResource resource) throws ResourceDuplicationException {

		if(resourceCache.contains(resource.getPath())) {
			throw new ResourceDuplicationException("Already existing resoure in same path!", resource.getPath());
		}

		resourceCache.put(resource.getPath(), resource);
	}

	public List<ResourceInstanceEvent> getInstanceEvents() {
		return instanceEvents;
	}

	public void flush(PathPattern matchPattern) {
		//getResource method will be invoked in flushContexts, so context must flush before the resources flush.
		flushSpaces(matchPattern);
		flushResources(matchPattern);
	}

	protected synchronized void flushResources(PathPattern matchPattern) {
		//Flush it's own cache for Reourses
		//To avoid the ConcurrentModificationException, we should do removing operations by following two step:
		//1 Find the match keys to be removed;
		Set<Path> keys = resourceCache.getPathsSet();
		List<Path> removingKeys = new ArrayList<Path>();
		if(matchPattern != null) {
			for(Path key : keys) {
				if(matchPattern.isMatch(key)) {
					removingKeys.add(key);
				}
			}
		}
		else {
			for(Path key : keys) {
				removingKeys.add(key);
			}
		}

		//2.Actually remove the values from targetCache
		if(removingKeys.size() > 0) {
			for(Path removingKey : removingKeys) {
				resourceCache.remove(removingKey);
			}
		}
	}

	/**
	 * Check the scopes' argument, which must contains two elements "setting" and "container".
	 * @param scopes
	 * @throws ConfigException
	 */
	protected void checkScopes(String[] scopes) throws ConfigException {
		boolean containerScopeFound = false;
		for(String scope : scopes) {
			if(CONTAINER_SCOPE.equals(scope)) {
				containerScopeFound = true;
				break;
			}
		}

		if(containerScopeFound == false) {
			throw new ConfigException("The scope \"" + CONTAINER_SCOPE + "\" must contains in the DefaultPathletContainer constructor scopes agruments!");
		}
	}

	protected void flushSpaces(PathPattern matchPattern) {
		synchronized(scopeSpaceMap) {
			for(Map.Entry<String, Map<Object, InstanceSpace>> entry : scopeSpaceMap.entrySet()) {

				//Flush the InstanceEvent, clear the data cached in InstanceEvent
				for(ResourceInstanceEvent instanceEvent : instanceEvents) {
					instanceEvent.flush(entry.getKey(), matchPattern);
				}

				for(InstanceSpace space : entry.getValue().values()) {
					space.flush(matchPattern);
				}
			}
		}
	}

	/**
	 * Actually get resource from sub ResourceFactory.
	 */
	private PathletResource fetchResource(Path fullPath) {

		ResourceConfig resourceConfig = configManager.getBeanConfig(fullPath);
		if(resourceConfig != null) {
			for(ConfigResourceFactory factory : configFactoriesMap.values()) {
				if (factory.isPathMatch(fullPath)) {
					//create resource by ResourceConfig.
					return factory.createResource(resourceConfig);
				}
			}
		}
		else {
			for (PathResourceFactory factory : factoriesMap.values()) {
				if (factory.isPathMatch(fullPath)) {
					//create resource by path
					PathletResource resource = factory.createResource(fullPath);
					if (resource != null) {
						return resource;
					}
				}
			}
		}

		throw new ResourceNotFoundException("Failed to find the matched ResourceFactory for resource path=" + fullPath);
	}



	/**
	 * Get the InstanceSpace instance by the scope and bindingObject.
	 * @param scope 作用域
	 * @param bindingObject 绑定对象(每个namespace都会绑定到一个对象上, 如：web应用中的每个ServletRequest绑定到一个InstanceSpace实例)
	 * @param create 如果InstanceSpace不存在，是否创建新的InstanceSpace
	 * @return InstanceSpace
	 */
	public InstanceSpace getSpace(String scope, Object bindingObject, boolean create) {
		synchronized(scopeSpaceMap) {
			Map<Object, InstanceSpace> spaces = scopeSpaceMap.get(scope);
			InstanceSpace space = spaces.get(bindingObject);

			//Create new InstanceSpace if the existent one could not be found.
			if(create == true && space == null) {
				space = new DefaultInstanceSpace(bindingObject, containerSpace, scope, this, this.spaceInstanceEvents);
				spaces.put(bindingObject, space);

				//Instance the "initInstance" property beans
				toInitInstance(space);
			}

			return space;
		}
	}


	/**
	 * Instance the "initInstance" property beans
	 */
	private void toInitInstance(InstanceSpace space) {
		List<Path> initPaths = initInstancePaths.get(space.getScope());
		if(initPaths != null && initPaths.size() > 0) {
			for(Path p : initPaths) {
				space.getInstance(p);
			}
		}

	}

	public void destroySpace(String scope, Object bindingObject) {
		synchronized(scopeSpaceMap) {
			Map<Object, InstanceSpace> spaces = scopeSpaceMap.get(scope);
			InstanceSpace space = spaces.get(bindingObject);
			space.flush(null);
			spaces.remove(bindingObject);
		}
	}

	public InstanceSpace getContainerSpace() {
		return containerSpace;
	}

	public void destroy() {
		flush(null);
	}


}
